/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var zrUtil = require("static/plugins/js/zrender/lib/core/util");

var matrix = require("static/plugins/js/zrender/lib/core/matrix");

var layoutUtil = require("../../util/layout");

var axisHelper = require("../../coord/axisHelper");

var ParallelAxis = require("./ParallelAxis");

var graphic = require("../../util/graphic");

var numberUtil = require("../../util/number");

var sliderMove = require("../../component/helper/sliderMove");

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/**
 * Parallel Coordinates
 * <https://en.wikipedia.org/wiki/Parallel_coordinates>
 */
var each = zrUtil.each;
var mathMin = Math.min;
var mathMax = Math.max;
var mathFloor = Math.floor;
var mathCeil = Math.ceil;
var round = numberUtil.round;
var PI = Math.PI;

function Parallel(parallelModel, ecModel, api) {
    /**
     * key: dimension
     * @type {Object.<string, module:echarts/coord/parallel/Axis>}
     * @private
     */
    this._axesMap = zrUtil.createHashMap();
    /**
     * key: dimension
     * value: {position: [], rotation, }
     * @type {Object.<string, Object>}
     * @private
     */

    this._axesLayout = {};
    /**
     * Always follow axis order.
     * @type {Array.<string>}
     * @readOnly
     */

    this.dimensions = parallelModel.dimensions;
    /**
     * @type {module:zrender/core/BoundingRect}
     */

    this._rect;
    /**
     * @type {module:echarts/coord/parallel/ParallelModel}
     */

    this._model = parallelModel;

    this._init(parallelModel, ecModel, api);
}

Parallel.prototype = {
    type: "parallel",
    constructor: Parallel,

    /**
     * Initialize cartesian coordinate systems
     * @private
     */
    _init: function (parallelModel, ecModel, api) {
        var dimensions = parallelModel.dimensions;
        var parallelAxisIndex = parallelModel.parallelAxisIndex;
        each(
            dimensions,
            function (dim, idx) {
                var axisIndex = parallelAxisIndex[idx];
                var axisModel = ecModel.getComponent("parallelAxis", axisIndex);

                var axis = this._axesMap.set(
                    dim,
                    new ParallelAxis(
                        dim,
                        axisHelper.createScaleByModel(axisModel),
                        [0, 0],
                        axisModel.get("type"),
                        axisIndex
                    )
                );

                var isCategory = axis.type === "category";
                axis.onBand = isCategory && axisModel.get("boundaryGap");
                axis.inverse = axisModel.get("inverse"); // Injection

                axisModel.axis = axis;
                axis.model = axisModel;
                axis.coordinateSystem = axisModel.coordinateSystem = this;
            },
            this
        );
    },

    /**
     * Update axis scale after data processed
     * @param  {module:echarts/model/Global} ecModel
     * @param  {module:echarts/ExtensionAPI} api
     */
    update: function (ecModel, api) {
        this._updateAxesFromSeries(this._model, ecModel);
    },

    /**
     * @override
     */
    containPoint: function (point) {
        var layoutInfo = this._makeLayoutInfo();

        var axisBase = layoutInfo.axisBase;
        var layoutBase = layoutInfo.layoutBase;
        var pixelDimIndex = layoutInfo.pixelDimIndex;
        var pAxis = point[1 - pixelDimIndex];
        var pLayout = point[pixelDimIndex];
        return (
            pAxis >= axisBase &&
            pAxis <= axisBase + layoutInfo.axisLength &&
            pLayout >= layoutBase &&
            pLayout <= layoutBase + layoutInfo.layoutLength
        );
    },
    getModel: function () {
        return this._model;
    },

    /**
     * Update properties from series
     * @private
     */
    _updateAxesFromSeries: function (parallelModel, ecModel) {
        ecModel.eachSeries(function (seriesModel) {
            if (!parallelModel.contains(seriesModel, ecModel)) {
                return;
            }

            var data = seriesModel.getData();
            each(
                this.dimensions,
                function (dim) {
                    var axis = this._axesMap.get(dim);

                    axis.scale.unionExtentFromData(
                        data,
                        data.mapDimension(dim)
                    );
                    axisHelper.niceScaleExtent(axis.scale, axis.model);
                },
                this
            );
        }, this);
    },

    /**
     * Resize the parallel coordinate system.
     * @param {module:echarts/coord/parallel/ParallelModel} parallelModel
     * @param {module:echarts/ExtensionAPI} api
     */
    resize: function (parallelModel, api) {
        this._rect = layoutUtil.getLayoutRect(
            parallelModel.getBoxLayoutParams(),
            {
                width: api.getWidth(),
                height: api.getHeight(),
            }
        );

        this._layoutAxes();
    },

    /**
     * @return {module:zrender/core/BoundingRect}
     */
    getRect: function () {
        return this._rect;
    },

    /**
     * @private
     */
    _makeLayoutInfo: function () {
        var parallelModel = this._model;
        var rect = this._rect;
        var xy = ["x", "y"];
        var wh = ["width", "height"];
        var layout = parallelModel.get("layout");
        var pixelDimIndex = layout === "horizontal" ? 0 : 1;
        var layoutLength = rect[wh[pixelDimIndex]];
        var layoutExtent = [0, layoutLength];
        var axisCount = this.dimensions.length;
        var axisExpandWidth = restrict(
            parallelModel.get("axisExpandWidth"),
            layoutExtent
        );
        var axisExpandCount = restrict(
            parallelModel.get("axisExpandCount") || 0,
            [0, axisCount]
        );
        var axisExpandable =
            parallelModel.get("axisExpandable") &&
            axisCount > 3 &&
            axisCount > axisExpandCount &&
            axisExpandCount > 1 &&
            axisExpandWidth > 0 &&
            layoutLength > 0; // `axisExpandWindow` is According to the coordinates of [0, axisExpandLength],
        // for sake of consider the case that axisCollapseWidth is 0 (when screen is narrow),
        // where collapsed axes should be overlapped.

        var axisExpandWindow = parallelModel.get("axisExpandWindow");
        var winSize;

        if (!axisExpandWindow) {
            winSize = restrict(
                axisExpandWidth * (axisExpandCount - 1),
                layoutExtent
            );
            var axisExpandCenter =
                parallelModel.get("axisExpandCenter") ||
                mathFloor(axisCount / 2);
            axisExpandWindow = [
                axisExpandWidth * axisExpandCenter - winSize / 2,
            ];
            axisExpandWindow[1] = axisExpandWindow[0] + winSize;
        } else {
            winSize = restrict(
                axisExpandWindow[1] - axisExpandWindow[0],
                layoutExtent
            );
            axisExpandWindow[1] = axisExpandWindow[0] + winSize;
        }

        var axisCollapseWidth =
            (layoutLength - winSize) / (axisCount - axisExpandCount); // Avoid axisCollapseWidth is too small.

        axisCollapseWidth < 3 && (axisCollapseWidth = 0); // Find the first and last indices > ewin[0] and < ewin[1].

        var winInnerIndices = [
            mathFloor(round(axisExpandWindow[0] / axisExpandWidth, 1)) + 1,
            mathCeil(round(axisExpandWindow[1] / axisExpandWidth, 1)) - 1,
        ]; // Pos in ec coordinates.

        var axisExpandWindow0Pos =
            (axisCollapseWidth / axisExpandWidth) * axisExpandWindow[0];
        return {
            layout: layout,
            pixelDimIndex: pixelDimIndex,
            layoutBase: rect[xy[pixelDimIndex]],
            layoutLength: layoutLength,
            axisBase: rect[xy[1 - pixelDimIndex]],
            axisLength: rect[wh[1 - pixelDimIndex]],
            axisExpandable: axisExpandable,
            axisExpandWidth: axisExpandWidth,
            axisCollapseWidth: axisCollapseWidth,
            axisExpandWindow: axisExpandWindow,
            axisCount: axisCount,
            winInnerIndices: winInnerIndices,
            axisExpandWindow0Pos: axisExpandWindow0Pos,
        };
    },

    /**
     * @private
     */
    _layoutAxes: function () {
        var rect = this._rect;
        var axes = this._axesMap;
        var dimensions = this.dimensions;

        var layoutInfo = this._makeLayoutInfo();

        var layout = layoutInfo.layout;
        axes.each(function (axis) {
            var axisExtent = [0, layoutInfo.axisLength];
            var idx = axis.inverse ? 1 : 0;
            axis.setExtent(axisExtent[idx], axisExtent[1 - idx]);
        });
        each(
            dimensions,
            function (dim, idx) {
                var posInfo = (
                    layoutInfo.axisExpandable
                        ? layoutAxisWithExpand
                        : layoutAxisWithoutExpand
                )(idx, layoutInfo);
                var positionTable = {
                    horizontal: {
                        x: posInfo.position,
                        y: layoutInfo.axisLength,
                    },
                    vertical: {
                        x: 0,
                        y: posInfo.position,
                    },
                };
                var rotationTable = {
                    horizontal: PI / 2,
                    vertical: 0,
                };
                var position = [
                    positionTable[layout].x + rect.x,
                    positionTable[layout].y + rect.y,
                ];
                var rotation = rotationTable[layout];
                var transform = matrix.create();
                matrix.rotate(transform, transform, rotation);
                matrix.translate(transform, transform, position); // TODO
                // tick等排布信息。
                // TODO
                // 根据axis order 更新 dimensions顺序。

                this._axesLayout[dim] = {
                    position: position,
                    rotation: rotation,
                    transform: transform,
                    axisNameAvailableWidth: posInfo.axisNameAvailableWidth,
                    axisLabelShow: posInfo.axisLabelShow,
                    nameTruncateMaxWidth: posInfo.nameTruncateMaxWidth,
                    tickDirection: 1,
                    labelDirection: 1,
                };
            },
            this
        );
    },

    /**
     * Get axis by dim.
     * @param {string} dim
     * @return {module:echarts/coord/parallel/ParallelAxis} [description]
     */
    getAxis: function (dim) {
        return this._axesMap.get(dim);
    },

    /**
     * Convert a dim value of a single item of series data to Point.
     * @param {*} value
     * @param {string} dim
     * @return {Array}
     */
    dataToPoint: function (value, dim) {
        return this.axisCoordToPoint(
            this._axesMap.get(dim).dataToCoord(value),
            dim
        );
    },

    /**
     * Travel data for one time, get activeState of each data item.
     * @param {module:echarts/data/List} data
     * @param {Functio} cb param: {string} activeState 'active' or 'inactive' or 'normal'
     *                            {number} dataIndex
     * @param {number} [start=0] the start dataIndex that travel from.
     * @param {number} [end=data.count()] the next dataIndex of the last dataIndex will be travel.
     */
    eachActiveState: function (data, callback, start, end) {
        start == null && (start = 0);
        end == null && (end = data.count());
        var axesMap = this._axesMap;
        var dimensions = this.dimensions;
        var dataDimensions = [];
        var axisModels = [];
        zrUtil.each(dimensions, function (axisDim) {
            dataDimensions.push(data.mapDimension(axisDim));
            axisModels.push(axesMap.get(axisDim).model);
        });
        var hasActiveSet = this.hasAxisBrushed();

        for (var dataIndex = start; dataIndex < end; dataIndex++) {
            var activeState;

            if (!hasActiveSet) {
                activeState = "normal";
            } else {
                activeState = "active";
                var values = data.getValues(dataDimensions, dataIndex);

                for (var j = 0, lenj = dimensions.length; j < lenj; j++) {
                    var state = axisModels[j].getActiveState(values[j]);

                    if (state === "inactive") {
                        activeState = "inactive";
                        break;
                    }
                }
            }

            callback(activeState, dataIndex);
        }
    },

    /**
     * Whether has any activeSet.
     * @return {boolean}
     */
    hasAxisBrushed: function () {
        var dimensions = this.dimensions;
        var axesMap = this._axesMap;
        var hasActiveSet = false;

        for (var j = 0, lenj = dimensions.length; j < lenj; j++) {
            if (
                axesMap.get(dimensions[j]).model.getActiveState() !== "normal"
            ) {
                hasActiveSet = true;
            }
        }

        return hasActiveSet;
    },

    /**
     * Convert coords of each axis to Point.
     *  Return point. For example: [10, 20]
     * @param {Array.<number>} coords
     * @param {string} dim
     * @return {Array.<number>}
     */
    axisCoordToPoint: function (coord, dim) {
        var axisLayout = this._axesLayout[dim];
        return graphic.applyTransform([coord, 0], axisLayout.transform);
    },

    /**
     * Get axis layout.
     */
    getAxisLayout: function (dim) {
        return zrUtil.clone(this._axesLayout[dim]);
    },

    /**
     * @param {Array.<number>} point
     * @return {Object} {axisExpandWindow, delta, behavior: 'jump' | 'slide' | 'none'}.
     */
    getSlidedAxisExpandWindow: function (point) {
        var layoutInfo = this._makeLayoutInfo();

        var pixelDimIndex = layoutInfo.pixelDimIndex;
        var axisExpandWindow = layoutInfo.axisExpandWindow.slice();
        var winSize = axisExpandWindow[1] - axisExpandWindow[0];
        var extent = [
            0,
            layoutInfo.axisExpandWidth * (layoutInfo.axisCount - 1),
        ]; // Out of the area of coordinate system.

        if (!this.containPoint(point)) {
            return {
                behavior: "none",
                axisExpandWindow: axisExpandWindow,
            };
        } // Conver the point from global to expand coordinates.

        var pointCoord =
            point[pixelDimIndex] -
            layoutInfo.layoutBase -
            layoutInfo.axisExpandWindow0Pos; // For dragging operation convenience, the window should not be
        // slided when mouse is the center area of the window.

        var delta;
        var behavior = "slide";
        var axisCollapseWidth = layoutInfo.axisCollapseWidth;

        var triggerArea = this._model.get("axisExpandSlideTriggerArea"); // But consider touch device, jump is necessary.

        var useJump = triggerArea[0] != null;

        if (axisCollapseWidth) {
            if (
                useJump &&
                axisCollapseWidth &&
                pointCoord < winSize * triggerArea[0]
            ) {
                behavior = "jump";
                delta = pointCoord - winSize * triggerArea[2];
            } else if (
                useJump &&
                axisCollapseWidth &&
                pointCoord > winSize * (1 - triggerArea[0])
            ) {
                behavior = "jump";
                delta = pointCoord - winSize * (1 - triggerArea[2]);
            } else {
                (delta = pointCoord - winSize * triggerArea[1]) >= 0 &&
                    (delta = pointCoord - winSize * (1 - triggerArea[1])) <=
                        0 &&
                    (delta = 0);
            }

            delta *= layoutInfo.axisExpandWidth / axisCollapseWidth;
            delta
                ? sliderMove(delta, axisExpandWindow, extent, "all") // Avoid nonsense triger on mousemove.
                : (behavior = "none");
        } // When screen is too narrow, make it visible and slidable, although it is hard to interact.
        else {
            var winSize = axisExpandWindow[1] - axisExpandWindow[0];
            var pos = (extent[1] * pointCoord) / winSize;
            axisExpandWindow = [mathMax(0, pos - winSize / 2)];
            axisExpandWindow[1] = mathMin(
                extent[1],
                axisExpandWindow[0] + winSize
            );
            axisExpandWindow[0] = axisExpandWindow[1] - winSize;
        }

        return {
            axisExpandWindow: axisExpandWindow,
            behavior: behavior,
        };
    },
};

function restrict(len, extent) {
    return mathMin(mathMax(len, extent[0]), extent[1]);
}

function layoutAxisWithoutExpand(axisIndex, layoutInfo) {
    var step = layoutInfo.layoutLength / (layoutInfo.axisCount - 1);
    return {
        position: step * axisIndex,
        axisNameAvailableWidth: step,
        axisLabelShow: true,
    };
}

function layoutAxisWithExpand(axisIndex, layoutInfo) {
    var layoutLength = layoutInfo.layoutLength;
    var axisExpandWidth = layoutInfo.axisExpandWidth;
    var axisCount = layoutInfo.axisCount;
    var axisCollapseWidth = layoutInfo.axisCollapseWidth;
    var winInnerIndices = layoutInfo.winInnerIndices;
    var position;
    var axisNameAvailableWidth = axisCollapseWidth;
    var axisLabelShow = false;
    var nameTruncateMaxWidth;

    if (axisIndex < winInnerIndices[0]) {
        position = axisIndex * axisCollapseWidth;
        nameTruncateMaxWidth = axisCollapseWidth;
    } else if (axisIndex <= winInnerIndices[1]) {
        position =
            layoutInfo.axisExpandWindow0Pos +
            axisIndex * axisExpandWidth -
            layoutInfo.axisExpandWindow[0];
        axisNameAvailableWidth = axisExpandWidth;
        axisLabelShow = true;
    } else {
        position =
            layoutLength - (axisCount - 1 - axisIndex) * axisCollapseWidth;
        nameTruncateMaxWidth = axisCollapseWidth;
    }

    return {
        position: position,
        axisNameAvailableWidth: axisNameAvailableWidth,
        axisLabelShow: axisLabelShow,
        nameTruncateMaxWidth: nameTruncateMaxWidth,
    };
}

var _default = Parallel;
module.exports = _default;
